﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using System.IO;
using System.Security.Cryptography;
using System.Linq;
using System.Threading.Tasks;
using System.Threading;

namespace PhotoManager
{
    public partial class MainForm : Form
    {
        //private List<string> m_directoryNames = new List<string>();
        private bool m_includeSubdirectories = true;
        private PhotoCollection m_collection;

        public MainForm()
        {
            InitializeComponent();
        }

        private void btnSearch_Click(object sender, EventArgs e)
        {
            CancellationTokenSource cts = new CancellationTokenSource();
            CancellationToken cancellationToken = cts.Token;

            progressBar1.Visible = true;

            // Build photo collection.
            List<string> directoryNames = new List<string>();
            foreach (string directoryName in listFolders.Items)
            {
                directoryNames.Add(directoryName);
            }
            PhotoCollection collection = BuildPhotoCollection(
                directoryNames, m_includeSubdirectories, txtDatabase.Text);

            // Group files by their content (hash).
            FileGroupSet partition = FindDistinctFiles(collection);

            // Find duplicate files.
            FileGroupSet wholeSet = FindDuplicateFiles(collection);
            long dupSize = (
                from @group in wholeSet.FileGroups
                select (@group.Files.Count - 1) * @group.Files[0].FileInfo.Length
                ).Sum();

            lblProgress.Text = string.Format("{0} duplicate file groups, {1:#,0} bytes.",
                wholeSet.FileGroups.Count, dupSize);

            progressBar1.Visible = false;

            // Arrange duplicate file groups by their location characteristic.
            var fileGroupSets =
                from fileGroup in wholeSet.FileGroups
                group fileGroup by fileGroup.GetLocationCharacteristic() into g
                select new FileGroupSet(g)
                {
                    Characteristic = g.Key,
                    DisplayName = GetDisplayNameFor(g.Key),
                };

            // Display them.
            cbUniqueFileGroup.Items.Clear();
            cbUniqueFileGroup.Items.Add(wholeSet);
            foreach (var fileGroupSet in fileGroupSets)
            {
                cbUniqueFileGroup.Items.Add(fileGroupSet);
            }

            // Cluster analysis of non-identical items.
            List<PhotoItem> distinctPhotos = (
                from @group in partition.FileGroups
                select @group.Files[0]
                ).ToList();

            FileGroupSet similarSet1 = ClusterAnalysis(
                distinctPhotos, 
                HistogramDistanceType.Bhattacharyya,
                0.05);
            cbUniqueFileGroup.Items.Add(similarSet1);

            FileGroupSet similarSet2 = ClusterAnalysis(
                distinctPhotos, 
                HistogramDistanceType.Simple,
                0.05);
            cbUniqueFileGroup.Items.Add(similarSet2);

            MessageBox.Show("Finish");
        }

        private static PhotoCollection BuildPhotoCollection(
            IEnumerable<string> directoryNames,
            bool includeSubdirectories,
            string databaseFileName)
            //IProgress<double> progress
        {
            // Get directories and subdirectories.
            List<string> folderNames = new List<string>();
            foreach (string folderName in directoryNames)
            {
                folderNames.Add(folderName);
                if (includeSubdirectories)
                {
                    folderNames.AddRange(Directory.GetDirectories(
                        folderName, "*", SearchOption.AllDirectories));
                }
            }
            folderNames = folderNames.Distinct(StringComparer.InvariantCultureIgnoreCase).ToList();

            // Create photo collection.
            PhotoCollection collection = new PhotoCollection(databaseFileName);

            // Scan the directories and add photos into the collection.
            int i = 0;
            foreach (string path in folderNames)
            {
                DirectoryInfo dir = new DirectoryInfo(path);
                // progressBar1.Value = progressBar1.Maximum * i / folderNames.Count;

                FileInfo[] files = dir.GetFiles("*.jpg");
                foreach (FileInfo file in files)
                {
                    collection.Add(file);
                }
                ++i;
            }
            return collection;
        }

        private static FileGroupSet FindDuplicateFiles(PhotoCollection collection)
        {
            var duplicateFileGroups =
                from photo in collection.Items
                group photo by photo.PhotoInfo.ContentHash into g
                where g.Count() > 1
                select new FileGroup(g);
            FileGroupSet set = new FileGroupSet(duplicateFileGroups)
            {
                Characteristic = new LocationCharacteristic(),
                DisplayName = "All Duplicates",
            };
            return set;
        }

        private static FileGroupSet FindDistinctFiles(PhotoCollection collection)
        {
            var distinctFileGroups =
                from photo in collection.Items
                group photo by photo.PhotoInfo.ContentHash into g
                select new FileGroup(g);
            return new FileGroupSet(distinctFileGroups);
        }

        private void backgroundSearchWorker_DoWork(object sender, DoWorkEventArgs e)
        {
#if false
            // full file name => photo record
            Dictionary<string, PhotoRecord> records = new Dictionary<string, PhotoRecord>();

            //string dbFileName = Path.Combine(dir.FullName, ".PhotoManager.ini");
            SearchParameters args = e.Argument as SearchParameters;

            // Get directories and subdirectories.
            var paths = new Dictionary<string, string>();
            foreach (string path in args.Paths)
            {
                paths[path.ToLowerInvariant()] = path;
                if (args.IncludeSubdirectories)
                {
                    string[] subdirs = Directory.GetDirectories(path, "*", SearchOption.AllDirectories);
                    foreach (string subdir in subdirs)
                        paths[subdir.ToLowerInvariant()] = subdir;
                }
            }

            // Create or open the database file.
            using (PhotoDatabase db = new PhotoDatabase(args.DatabaseFileName))
            {
                // Scan the directories and cache photo information.
                int i = 0;
                foreach (string path in paths.Values)
                {
                    if (backgroundSearchWorker.CancellationPending)
                        return;

                    DirectoryInfo dir = new DirectoryInfo(path);
                    backgroundSearchWorker.ReportProgress((int)(i * 100 / paths.Count), null);

                    FileInfo[] files = dir.GetFiles("*.jpg");
                    foreach (FileInfo file in files)
                    {
                        if (backgroundSearchWorker.CancellationPending)
                            return;
                        PhotoRecord r = db.FindRecord(file);
                        if (r == null)
                        {
                            r = PhotoRecord.CreateFromFile(file);
                            db.AddRecord(r);
                        }
                        records[file.FullName] = r;
                    }
                    ++i;
                }
            }
            e.Result = records;
#endif
        }

        private void backgroundSearchWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
        {
#if false
            // FileName => PhotoRecord
            var records = (Dictionary<string, PhotoRecord>)e.Result;

            // Group the files by their content hash, and keep files with
            // more than one copies.
            dupFileGroups = (
                from kv in records
                let fileName = kv.Key
                let record = kv.Value
                group new FileInfo(fileName) by record.ContentHash into g
                where g.Count() > 1
                select new FileGroup(g)
                ).ToList();
            FileGroupSet wholeSet = new FileGroupSet(dupFileGroups)
            {
                Characteristic = new LocationCharacteristic(),
                DisplayName = "All Duplicates",
            };

            // Compute total duplicate size.
            long dupSize = (
                from fileGroup in dupFileGroups
                select (fileGroup.Files.Count - 1) * fileGroup.Files[0].Length
                ).Sum();

            lblProgress.Text = string.Format("{0} duplicate file groups, {1:#,0} bytes.",
                dupFileGroups.Count, dupSize);
            progressBar1.Visible = false;

            // Arrange duplicate file groups by their location characteristic.
            var fileGroupSets =
                from fileGroup in dupFileGroups
                group fileGroup by fileGroup.GetLocationCharacteristic() into g
                select new FileGroupSet(g)
                {
                    Characteristic = g.Key,
                    DisplayName = GetDisplayNameFor(g.Key),
                };

            // Display them.
            cbUniqueFileGroup.Items.Clear();
            cbUniqueFileGroup.Items.Add(wholeSet);
            foreach (var fileGroupSet in fileGroupSets)
            {
                cbUniqueFileGroup.Items.Add(fileGroupSet);
            }

            // Cluster analysis.
            ClusterAnalysis(records);
#endif
        }

        private static FileGroupSet ClusterAnalysis(
            IEnumerable<PhotoItem> collection,
            HistogramDistanceType distanceType,
            double threshold)
        {
            List<PhotoItem> photos = collection.ToList();
            int n = photos.Count;
            double[,] distance = new double[n, n];
            for (int i = 0; i < n; i++)
            {
                for (int j = i + 1; j < n; j++)
                {
                    ColorDistribution d1 = photos[i].PhotoInfo.ColorDistribution;
                    ColorDistribution d2 = photos[j].PhotoInfo.ColorDistribution;
                    double d = ColorDistribution.GetDistance(d1, d2, distanceType);
                    distance[i, j] = d;
                }
            }

            FileGroupSet set = new FileGroupSet();
            set.DisplayName = "Visually Similar";

            for (int i = 0; i < n; i++)
            {
                for (int j = i + 1; j < n; j++)
                {
                    if (distance[i, j] <= threshold)
                    {
                        FileGroup fileGroup = new FileGroup();
                        fileGroup.Files.Add(photos[i]);
                        fileGroup.Files.Add(photos[j]);
                        set.FileGroups.Add(fileGroup);
                    }
                }
            }
            return set;
        }

        private static string GetDisplayNameFor(LocationCharacteristic c)
        {
            if (c.FolderNames == null)
                return "All Duplicates";
            else
                return string.Join(" & ", c.FolderNames.Select(s => Path.GetFileName(s)));
        }

        private void backgroundSearchWorker_ProgressChanged(object sender, ProgressChangedEventArgs e)
        {
            progressBar1.Value = progressBar1.Maximum * e.ProgressPercentage / 100;
            //lblProgress.Text = di.Directory.FullName;
        }

        private void lvDuplicates_SelectedIndexChanged(object sender, EventArgs e)
        {
            listDetails.Items.Clear();
            if (lvDuplicates.SelectedIndices.Count == 1)
            {
                FileGroup group = (FileGroup)lvDuplicates.SelectedItems[0].Tag;
                string[] fileNames = (
                    from photo in @group.Files
                    select photo.FileInfo.FullName
                    ).ToArray();
                listDetails.Items.AddRange(fileNames);
                DisplayImages(fileNames);
            }
        }

        private void DisplayImages(IList<string> fileNames)
        {
            if (fileNames == null)
                throw new ArgumentNullException("fileNames");

            using (pictureBox1.Image)
                pictureBox1.Image = null;
            using (pictureBox2.Image)
                pictureBox2.Image = null;

            if (fileNames.Count >= 1)
            {
                Image image = Bitmap.FromFile(fileNames[0]);
                pictureBox1.Image = image;
            }
            if (fileNames.Count >= 2)
            {
                Image image = Bitmap.FromFile(fileNames[1]);
                pictureBox2.Image = image;
            }
        }

        private void MainForm_Load(object sender, EventArgs e)
        {
            //lvDuplicates.SetWindowTheme("explorer");
            listFolders_SelectedIndexChanged(null, null);
        }

        private void btnAddFolder_Click(object sender, EventArgs e)
        {
            if (folderBrowserDialog1.ShowDialog(this) != DialogResult.OK)
                return;

            string path = folderBrowserDialog1.SelectedPath;
            listFolders.Items.Add(path);
        }

        private void listFolders_SelectedIndexChanged(object sender, EventArgs e)
        {
            int k = listFolders.SelectedIndex;
            int n = listFolders.Items.Count;
            btnRemoveFolder.Enabled = (k >= 0);
            btnMoveUp.Enabled = (k > 0);
            btnMoveDown.Enabled = (k >= 0 && k < n - 1);
        }

        private void btnRemoveFolder_Click(object sender, EventArgs e)
        {
            if (listFolders.SelectedIndex >= 0)
                listFolders.Items.RemoveAt(listFolders.SelectedIndex);
        }

        private FileGroupSet GetCurrentFileGroupSet()
        {
            return (cbUniqueFileGroup.SelectedItem as FileGroupSet);
        }

        private void btnCleanUp_Click(object sender, EventArgs e)
        {
            int dupCount = 0;
            long dupSize = 0;
            bool rememberChoice = true;

            FolderPrecedence preference = new FolderPrecedence();
            FileGroupSet set = GetCurrentFileGroupSet();

            for (int iGroup = 0; iGroup < set.FileGroups.Count; iGroup++)
            {
                List<string> dupFiles = new List<string>(set.FileGroups[iGroup].FileNames);

                // Figure out which file to keep and which to delete.
                int k = preference.FindPreferredItem(dupFiles);
                if (k == -1) // no preferred file; ask the user
                {
                    RemoveDuplicateForm f = new RemoveDuplicateForm();
                    f.Text = string.Format("Remove Duplicates ({0} of {1})",
                        iGroup + 1, set.FileGroups.Count);
                    f.RememberChoice = rememberChoice;
                    f.DuplicateFiles = dupFiles.ToArray();
                    f.FolderPrecedence = preference;
                    if (f.ShowDialog(this) == DialogResult.Cancel)
                        break;

                    bool[] keep = f.DuplicateChecked;

                    // For each pair of (a, b) where a is to keep and
                    // b is to remove, create a rule using the parent
                    // directories of a and b.
                    rememberChoice = f.RememberChoice;
                    if (rememberChoice)
                    {
                        for (int i = 0; i < keep.Length; i++)
                        {
                            for (int j = 0; j < keep.Length; j++)
                            {
                                if (keep[i] && !keep[j])
                                {
                                    string dirName1 = Path.GetDirectoryName(dupFiles[i]);
                                    string dirName2 = Path.GetDirectoryName(dupFiles[j]);
                                    if (dirName1 != dirName2)
                                        preference.SetRelation(dirName1, dirName2);
                                }
                            }
                        }
                    }
                }

                // Check whether the files are indeed duplicate by
                // comparing them byte-by-byte.
                for (int i = 1; i < dupFiles.Count; i++)
                {
                    if (backgroundCleanUpWorker.CancellationPending)
                        return;
                    //    if (!CompareFiles(dupFiles[0], dupFiles[i]))
                    //        dupFiles.RemoveAt(i--);
                }
                if (dupFiles.Count <= 1)
                    continue;

                dupCount += dupFiles.Count - 1;
                dupSize += (dupFiles.Count - 1) * (new FileInfo(dupFiles[0]).Length);
            }
            MessageBox.Show(string.Format(
                "Moved {0} files to the Recycle Bin. This saves {1:#,0} bytes of disk space.",
                dupCount, dupSize));
        }

        private static bool CompareFiles(string fileName1, string fileName2)
        {
            using (Stream stream1 = new FileStream(fileName1, FileMode.Open, FileAccess.Read, FileShare.Read))
            using (Stream stream2 = new FileStream(fileName2, FileMode.Open, FileAccess.Read, FileShare.Read))
            {
                if (stream1.Length != stream2.Length)
                    return false;

                const int bufferSize = 0x10000;
                byte[] buffer1 = new byte[bufferSize];
                byte[] buffer2 = new byte[bufferSize];

                while (true)
                {
                    int n1 = stream1.Read(buffer1, 0, bufferSize);
                    int n2 = stream2.Read(buffer2, 0, bufferSize);
                    if (n1 != n2)
                        return false;
                    if (n1 == 0)
                        return true;

                    for (int i = 0; i < n1; i++)
                    {
                        if (buffer1[i] != buffer2[i])
                            return false;
                    }
                }
            }
        }

        private void backgroundCleanUpWorker_DoWork(object sender, DoWorkEventArgs e)
        {
#if false
            CleanUpParameters args = (CleanUpParameters)e.Argument;

            foreach (List<string> dupFiles in args.DuplicateFiles)
            {
                // Figure out which file to keep and which to delete.

                // Check whether the files are indeed duplicate by
                // comparing them byte-by-byte.
                for (int i = 1; i < dupFiles.Count; i++)
                {
                    if (backgroundCleanUpWorker.CancellationPending)
                        return;
                    if (!CompareFiles(dupFiles[0], dupFiles[i]))
                        dupFiles.RemoveAt(i--);
                }
                if (dupFiles.Count <= 1)
                    continue;

                dupCount += dupFiles.Count - 1;
                dupSize += (dupFiles.Count - 1) * (new FileInfo(dupFiles[0]).Length);
            }
            MessageBox.Show(string.Format(
                "Moved {0} files to the Recycle Bin. This saves {1:#,0} bytes of disk space.",
                dupCount, dupSize));
#endif
        }

        private void btnMoveUp_Click(object sender, EventArgs e)
        {
            int k = listFolders.SelectedIndex;
            if (k > 0)
            {
                object s = listFolders.Items[k];
                listFolders.Items[k] = listFolders.Items[k - 1];
                listFolders.Items[k - 1] = s;
                listFolders.SelectedIndex = k - 1;
            }
        }

        private void btnMoveDown_Click(object sender, EventArgs e)
        {
            int k = listFolders.SelectedIndex;
            if (k >= 0 && k < listFolders.Items.Count - 1)
            {
                object s = listFolders.Items[k];
                listFolders.Items[k] = listFolders.Items[k + 1];
                listFolders.Items[k + 1] = s;
                listFolders.SelectedIndex = k + 1;
            }
        }

        private void btnOpenContainingFolder_Click(object sender, EventArgs e)
        {
            if (listDetails.SelectedIndex>=0)
            {
                string fileName = (string)listDetails.SelectedItem;
                ShellHelper.ShowFileInExplorer(fileName);
            }
        }

        private void cbUniqueFileGroup_SelectedIndexChanged(object sender, EventArgs e)
        {
            if (cbUniqueFileGroup.SelectedIndex >= 0)
            {
                FileGroupSet set = (FileGroupSet)cbUniqueFileGroup.SelectedItem;

                lvDuplicates.Items.Clear();
                foreach (var fileGroup in set.FileGroups)
                {
                    var photo = fileGroup.Files[0];
                    ListViewItem item = new ListViewItem();
                    item.Text =  photo.FileInfo.Name;
                    item.SubItems.Add(fileGroup.Files.Count.ToString());
                    item.SubItems.Add(photo.FileInfo.Length.ToString("#,0"));
                    item.Tag = fileGroup;
                    lvDuplicates.Items.Add(item);
                }
            }
        }

        private void btnClustering_Click(object sender, EventArgs e)
        {
        }

    }

    class FileGroup
    {
        private readonly List<PhotoItem> m_files = new List<PhotoItem>();

        public List<PhotoItem> Files
        {
            get { return m_files; }
        }

        public IEnumerable<string> FileNames
        {
            get { return m_files.Select(photo => photo.FileInfo.FullName); }
        }

        public FileGroup() { }

        public FileGroup(IEnumerable<PhotoItem> files)
        {
            if (files == null)
                throw new ArgumentNullException("files");
            m_files.AddRange(files);
        }

        /// <summary>
        /// Gets a string that describes the characteristic of the locations
        /// of the files in this group.
        /// </summary>
        /// <returns></returns>
        public LocationCharacteristic GetLocationCharacteristic()
        {
            return new LocationCharacteristic(m_files);
        }
    }

    class LocationCharacteristic : 
        IComparable<LocationCharacteristic>,
        IEquatable<LocationCharacteristic>
    {
        private readonly string[] m_folderNames;
        private readonly string m_key;

        public LocationCharacteristic() 
        {
            m_key = "";
        }

        public LocationCharacteristic(IEnumerable<FileInfo> files)
        {
            if (files != null)
            {
                m_folderNames = (
                    from file in files
                    orderby file.DirectoryName.ToLowerInvariant()
                    select file.DirectoryName
                    ).Distinct().ToArray();
            }
            m_key = string.Join("?", m_folderNames.Select(s => s.ToLowerInvariant()));
        }

        public LocationCharacteristic(IEnumerable<PhotoItem> photos)
            : this(photos.Select(photo => photo.FileInfo))
        {
        }

        public IEnumerable<string> FolderNames
        {
            get { return m_folderNames; }
        }

        public override int GetHashCode()
        {
            return m_key.GetHashCode();
        }

        public int CompareTo(LocationCharacteristic other)
        {
           return this.m_key.CompareTo(other.m_key);
        }

        public bool Equals(LocationCharacteristic other)
        {
            return this.m_key == other.m_key;
            //throw new NotImplementedException();
        }
    }

    class FileGroupSet
    {
        private readonly List<FileGroup> m_fileGroups = new List<FileGroup>();

        public LocationCharacteristic Characteristic { get; set; }
        public string DisplayName { get; set; }

        public FileGroupSet() { }
        public FileGroupSet(IEnumerable<FileGroup> fileGroups)
        {
            if (fileGroups == null)
                throw new ArgumentNullException("fileGroups");
            m_fileGroups.AddRange(fileGroups);
        }

        public IList<FileGroup> FileGroups
        {
            get { return m_fileGroups; }
        }

        public override string ToString()
        {
            return string.Format("{0} ({1})", DisplayName, m_fileGroups.Count);
        }
    }

    class SearchParameters
    {
        public bool IncludeSubdirectories { get; set; }
        public string[] Paths { get; set; }
        public string DatabaseFileName { get; set; }
    }

    class CleanUpParameters
    {
        public List<List<string>> DuplicateFiles { get; set; }
    }

    class CleanUpResults
    {
        public int DuplicateCount { get; set; }
        public long DuplicateSize { get; set; }
    }
}
